Entity Framework 中 DateTime 時區問題與解決方案
TLDR
DateTime的Kind屬性若為Unspecified,在進行時區轉換時會產生非預期的偏差。- 資料庫欄位(如
datetime2)不儲存時區資訊,導致 Entity Framework 取出資料時Kind預設為Unspecified,進而導致前端顯示時間錯誤。 - 解決方案為使用
ValueConverter,在寫入資料庫前確保時間為 UTC,並在讀取時強制將Kind標記為Utc。 - 推薦使用
DbContext.ConfigureConventions()統一設定,避免手動為每個屬性定義轉換器。
DateTime 的時區格式問題
在 .NET 中,DateTime 的 Kind 屬性決定了時間處理的行為。當 Kind 為 Unspecified 時,呼叫 ToLocalTime() 或 ToUniversalTime() 會導致系統根據當前環境進行錯誤的時區偏移計算。
什麼情況下會遇到這個問題: 當開發者直接使用 DateTime 物件,且未明確指定或轉換其 Kind 屬性,導致系統在進行時間運算時誤判時區。
以下為 Kind 對轉換行為的影響:
Local:呼叫ToLocalTime()不會改變時間。Utc:呼叫ToUniversalTime()不會改變時間。Unspecified:呼叫ToLocalTime()會假設原時間為 UTC 並加上時區偏移;呼叫ToUniversalTime()則假設原時間為本機時間並減去偏移。
TIP
為避免此類問題,可參考 ABP.IO 框架的 IClock 實作,透過比對 Kind 來進行標準化處理。
Entity Framework 使用 DateTime 的時區問題
當使用不包含時區資訊的資料庫型別(如 datetime、datetime2)時,Entity Framework 取出的資料其 Kind 永遠為 Unspecified,這導致序列化傳給前端時,時間字串缺少代表 UTC 的 Z 結尾,造成前端顯示的時間與預期有 8 小時誤差。
什麼情況下會遇到這個問題: 當專案採用 Code First 或反向工程,且資料庫欄位未儲存時區資訊,導致 EF Core 讀取資料後,物件的 Kind 屬性無法正確反映 UTC 狀態。
解決方案:使用 ValueConverter
透過 ValueConverter,可以在資料寫入時強制轉換為 UTC,並在讀取時強制標記為 Utc。
1. 定義轉換器
csharp
public class UtcDateTimeValueConverter : ValueConverter<DateTime, DateTime> {
public UtcDateTimeValueConverter()
: base(v => ToDb(v), v => FromDb(v)) {
}
private static DateTime ToDb(DateTime dateTime) {
return dateTime.Kind == DateTimeKind.Utc ? dateTime : dateTime.ToUniversalTime();
}
private static DateTime FromDb(DateTime dateTime) {
return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
}
}2. 全域設定(推薦)
在 .NET 6 以上版本,建議使用 ConfigureConventions 統一處理所有 DateTime 欄位,無需逐一設定:
csharp
public partial class MyDbContext : DbContext {
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) {
ArgumentNullException.ThrowIfNull(configurationBuilder);
configurationBuilder.Properties<DateTime>().HaveConversion<UtcDateTimeValueConverter>();
}
}若使用反向工程產生的 DbContext,可利用 OnModelCreatingPartial 方法進行擴充:
csharp
partial void OnModelCreatingPartial(ModelBuilder modelBuilder) {
foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) {
foreach (IMutableProperty property in entityType.GetProperties()) {
if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?)) {
property.SetValueConverter(typeof(UtcDateTimeValueConverter));
}
}
}
}TIP
本篇的完整可執行範例:CloudyWing/EfCoreBehaviorSample。
異動歷程
- 初版文件建立。
- 補上對應 GitHub 範例專案連結。